/*
 * $Id: MagmonIriData.java,v 1.4 2009/08/21 14:50:48 navdeep_mahajan Exp $
 * 
 * Copyright (C) 2008 General Electric Company. All Rights Reserved.
 * 
 * This software is the confidential and proprietary information of the General
 * Electric Company (GE). You shall not disclose this software and shall use it
 * only in accordance with the terms of the license agreement you entered into
 * with GE.
 * 
 * GE MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
 * NON-INFRINGEMENT. GE SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING, OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES.
 */
package com.ge.healthcare.autosc.processor.magmon;

import java.text.DecimalFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.StringTokenizer;

import org.springframework.util.Assert;

import com.ge.healthcare.autosc.common.database.dao.IStagedIriDataDAO;
import com.ge.healthcare.autosc.common.database.pojo.StagedIriData;
import com.ge.healthcare.autosc.common.exceptions.BasicException;
import com.ge.healthcare.autosc.common.util.TimeFormatStandard;
import com.ge.healthcare.autosc.processor.shemjob.guidance.dataset.MagnetDataChannelDef;

/**
 * Class for extracting the IRI data from the last line of the ASCII magnet
 *  data.  The extracted data is available as name/value pairs and is also
 *  written to a database.
 *
 * @author Daniel S. Horwitz, Compuware Corp.
 *
 * @version AutoSC P5P6
 */
public class MagmonIriData implements IMagmonIriData {
	
	private static IStagedIriDataDAO stagedIriDataDAO = null;
	
	private static IMagmonIriData magMonIriData = null;
	
    /**
     * Channel information for the named channels in the IRI data.
     */
    private MagnetDataChannelDef[] theChannelDefList = null;

    /**
     * Date read from the source ASCII magnet data record.
     */
    private Date theDate = null;

    /**
     * Date read from the source ASCII magnet data record, stored as
     *  name/value pairs.  This is the beginning of the string returned
     *  when getting the magnet data as name/value pairs.
     */
    private String dateAsNameValuePairs = null;

    /**
     * Channel value fields read from the source ASCII magnet data record.
     *  The length of the array matches the number of channel value fields
     *  present in an ASCII magnet data record.
     */
    private float[] channelValues = new float[19];

    /**
     * Channel status fields read from the source ASCII magnet data record.
     *  The length of the array matches the number of channel status fields
     *  present in an ASCII magnet data record.
     */
    private int[] channelStatus = new int[16];

    /**
     * Size of the buffer used for representing the IRI data as name/value
     *  pairs.  For convenience, it is also used as the size of the buffers
     *  used in other parts of this class.
     */
    private static final int IRI_BUFFER_SIZE = 320;

    /**
     * Object used for formatting numbers where the format is supplied when
     *  needed.  Because locale information is accessed when constructing the
     *  object, a single format object is used for each instance of this
     *  class.
     */
    private DecimalFormat theFormatter = new DecimalFormat();

    /**
     * System Id
     */
    private String systemId = null;
    
    /**
     * This is for Spring configuration. PLEASE USE OTHER CONSTRUCTORS
     */
    public MagmonIriData() {}
    
    /**
     * Create an IRI data instance from a line of ASCII magnet data.
     *
     * @param sourceRecord The source record from the source ASCII magnet data.
     *
     * @param systemId The identifier for the system.  This is also known
     *  as the GeCares value.
     *
     * @param channelDefList Channel information for the named channels
     *  in the IRI data.
     *
     * @param helper Object providing the utility objects and methods.
     *
     * @exception AscServiceAbortException If an error is detected in
     *  the format of the magnet data or a channel index is out of range.
     */
    public MagmonIriData( String sourceRecord,
                          String systemId,
                          MagnetDataChannelDef[] channelDefList )
        throws BasicException
    {
    	this.systemId = systemId;
        theChannelDefList = channelDefList;
        parseMagnetData( sourceRecord );
        verifyChannelIndices();
    }

    public void init() {
    	Assert.notNull(stagedIriDataDAO,
			"StagedIriDataDAO object is required. Use setStagedIriDataDAO() to provide one.");
    }
    
    /**
     * Send the IRI data to the database. This method is used by Spring 
     * to create new transaction for this method.
     * @param theChannelDefList	Channel information for the named channels in the IRI data
     * @param channelValues	Channel value fields read from the source ASCII magnet data record
     * @param theDate	Date read from the source ASCII magnet data record
     * @throws BasicException
     */
    public void writeToDatabase(MagnetDataChannelDef[] theChannelDefList,
    							float[] channelValues,
    							Date theDate) {
        for ( int idx = 0; idx < theChannelDefList.length; idx += 1 )
        {
            MagnetDataChannelDef chan = theChannelDefList[idx];
            StagedIriData iriData = new StagedIriData();
            iriData.setGecares(systemId);
            iriData.setCreatedTime(TimeFormatStandard.getNowMillis());
            iriData.setDataTimestamp(theDate);
            iriData.setChannelName(chan.getChannelName());
            
            final float rawValue = channelValues[ chan.getChannelIndex() ];
            double value = Math.max( 0.0f, rawValue );
            value = Math.min( value, chan.getChannelMaxValue() );
            iriData.setChannelValue(value);
            if(isEnabled(chan)) {
            	iriData.markChannelEnabled();
            } else {
            	iriData.markChannelEnabled();
            }
            stagedIriDataDAO.save(iriData);
        }
    }
    
    /**
     * Send the IRI data to the database. This method is used in Spring to
     * generate a new transaction for the operation.
     *
     * @exception AscServiceAbortException If an error is detected.
     */
    public void writeToDatabaseWithTransaction() throws BasicException {
    	if(magMonIriData != null) {
    		magMonIriData.writeToDatabase(this);
    	} else {
    		throw new BasicException("MagmonIriData instance is not defined/configured");
    	}
    }

    /**
     * Send the IRI data to the database.
     */
    public void writeToDatabase() throws BasicException {
    	writeToDatabase(theChannelDefList, channelValues, theDate);
    }
    
    /**
     * This is a Spring transaction wrapper
     * @param magMonIriData
     * @throws BasicException
     */
    public void writeToDatabase(IMagmonIriData magMonIriData) 
		throws BasicException {
    	magMonIriData.writeToDatabase();
    }


    
	public void setStagedIriDataDAO(IStagedIriDataDAO stagedIriDataDAO) {
		MagmonIriData.stagedIriDataDAO = stagedIriDataDAO;
	}
    
	public void setMagmonIriData(IMagmonIriData magmonIriData) {
		magMonIriData = magmonIriData;
	}
	
    /**
     * Parse a line of ASCII magnet data.
     *
     * @param sourceRecord The source record from the source ASCII magnet data.
     *
     * @exception AscServiceAbortException If an error is detected in
     *  the format of the magnet data.
     */
    private void parseMagnetData( String sourceRecord )
        throws BasicException
    {
        StringTokenizer theTokenizer =
            new StringTokenizer( sourceRecord, " ", false );
        if ( !theTokenizer.hasMoreTokens() )
        {
            reportFormatError( "Missing time-date stamp", null, sourceRecord );
        }
        String magTimeDate = theTokenizer.nextToken();
        verifyTimeDateDelimiters( magTimeDate, sourceRecord );
        theDate = getDateObjectFrom( magTimeDate, sourceRecord );
        dateAsNameValuePairs = getNameValuePairsFrom( magTimeDate );
        for ( int idx1 = 0; idx1 < channelValues.length; idx1 += 1 )
        {
            channelValues[idx1] = getFloatFrom( theTokenizer, sourceRecord );
        }
        for ( int idx2 = 0; idx2 < channelStatus.length; idx2 += 1 )
        {
            channelStatus[idx2] = getIntFrom( theTokenizer, sourceRecord );
        }
    }

    /**
     * Verify that the valid delimiters are found in a magnet data
     *  time-date stamp.  It also verifies the length of the field.
     *
     * @param timeDate The time-date stamp to be verified.
     *
     * @param sourceRecord The source record from the source ASCII magnet data.
     *
     * @exception AscServiceAbortException If an error is detected in the
     *  format of the magnet data time-date stamp.
     */
    private void verifyTimeDateDelimiters( String timeDate,
                                           String sourceRecord )
        throws BasicException
    {
        if ( timeDate.length() != 16 )
        {
            reportFormatError( "Time-date stamp is wrong length",
                               timeDate,
                               sourceRecord );
        }
        if ( (timeDate.charAt(4) != '/') ||
             (timeDate.charAt(7) != '/') ||
             (timeDate.charAt(10) != '!') ||
             (timeDate.charAt(13) != ':') )
        {
            reportFormatError( "Bad delimiter in time-date stamp",
                               timeDate,
                               sourceRecord );
        }
    }

    /**
     * Generate a Date object from a date string in the format
     *  yyyy/mm/dd!hh:mm.
     *
     * @param timeDate The time-date stamp to be converted to a Date object.
     *
     * @param sourceRecord The source record from the source ASCII magnet data.
     *
     * @return The Date object generated from the string.
     *
     * @exception AscServiceAbortException If one of the numeric fields in
     *  the time-date stamp is not a number.
     */
    private Date getDateObjectFrom( String timeDate, String sourceRecord )
        throws BasicException
    {
        final int year = getIntFrom( 0, 4, "Year", timeDate, sourceRecord );
        final int month = getIntFrom( 5, 7, "Month", timeDate, sourceRecord );
        final int day = getIntFrom( 8, 10, "Day", timeDate, sourceRecord );
        final int hour = getIntFrom( 11, 13, "Hour", timeDate, sourceRecord );
        final int minute =
            getIntFrom( 14, 16, "Minute", timeDate, sourceRecord );
        GregorianCalendar myCalendar =
            new GregorianCalendar( year, month - 1, day, hour, minute, 0 );
        return myCalendar.getTime();
    }

    /**
     * Generate the name/value pairs from a date string in the format
     *  yyyy/mm/dd!hh:mm.
     *
     * @param timeDate The time-date stamp to be converted to name/value pairs.
     *
     * @return The date string converted to name/value pairs.
     *  This is the beginning of the string returned when getting the
     *  magnet data as name/value pairs.
     */
    private String getNameValuePairsFrom( String timeDate )
    {
        StringBuffer buffer = new StringBuffer( IRI_BUFFER_SIZE );
        buffer.append( "year=" );
        buffer.append( timeDate.charAt( 0 ) );
        buffer.append( timeDate.charAt( 1 ) );
        buffer.append( timeDate.charAt( 2 ) );
        buffer.append( timeDate.charAt( 3 ) );
        buffer.append( "&month=" );
        buffer.append( timeDate.charAt( 5 ) );
        buffer.append( timeDate.charAt( 6 ) );
        buffer.append( "&day=" );
        buffer.append( timeDate.charAt( 8 ) );
        buffer.append( timeDate.charAt( 9 ) );
        buffer.append( "&hour=" );
        buffer.append( timeDate.charAt( 11 ) );
        buffer.append( timeDate.charAt( 12 ) );
        buffer.append( "&minute=" );
        buffer.append( timeDate.charAt( 14 ) );
        buffer.append( timeDate.charAt( 15 ) );
        return buffer.toString();
    }

    /**
     * Get a floating-point value from the next token of the specified source.
     *
     * @param theTokenizer The tokenizer used for getting blank-delimited
     *  tokens from the source record.  Upon exiting this method, the
     *  tokenizer will point to the next unused token.
     *
     * @param sourceRecord The source record from the source magnet data.
     *
     * @return The next token, interpreted as a float.
     *
     * @exception AscServiceAbortException If the next token is missing or
     *  is not a correctly formatted.
     */
    private float getFloatFrom( StringTokenizer theTokenizer,
                                String sourceRecord )
        throws BasicException
    {
        if ( !theTokenizer.hasMoreTokens() )
        {
            reportFormatError( "Missing floating-point value",
                               null,
                               sourceRecord );
        }
        String theToken = theTokenizer.nextToken();
        float value = 0.0f;
        try
        {
            value = Float.parseFloat( theToken );
        }
        catch ( NumberFormatException e )
        {
            reportFormatError( "Not a valid floating-point value",
                               theToken,
                               sourceRecord );
        }
        return value;
    }

    /**
     * Get an integer value from the next token of the specified source.
     *
     * @param theTokenizer The tokenizer used for getting blank-delimited
     *  tokens from the source record.  Upon exiting this method, the
     *  tokenizer will point to the next unused token.
     *
     * @param sourceRecord The source record from the source magnet data.
     *
     * @return The next token, interpreted as an integer.
     *
     * @exception AscServiceAbortException If the next token is missing or
     *  is not correctly formatted.
     */
    private int getIntFrom( StringTokenizer theTokenizer, String sourceRecord )
        throws BasicException
    {
        if ( !theTokenizer.hasMoreTokens() )
        {
            reportFormatError( "Missing integer value",
                               null,
                               sourceRecord );
        }
        String theToken = theTokenizer.nextToken();
        int value = 0;
        try
        {
            value = Integer.parseInt( theToken, 10 );
        }
        catch ( NumberFormatException e )
        {
            reportFormatError( "Not a valid integer value",
                               theToken,
                               sourceRecord );
        }
        return value;
    }

    /**
     * Get an integer from a specified substring of a time-date string.
     *
     * @param beginIndex The beginning index of the specified substring.
     *
     * @param endIndex The end index of the specified substring.
     *  The substring begins at the specified beginIndex and extends to
     *  the character at index endIndex - 1.
     *
     * @param fieldName Name of the field within the time-date stamp.
     *
     * @param timeDate The time-date stamp from which to get the substring.
     *  The length of this string has already been verified.
     *
     * @param sourceRecord The source record from the source ASCII magnet data.
     *
     * @return The specified substring as an integer.
     *
     * @exception AscServiceAbortException If the specified substring of
     *  the time-date stamp is not a number.
     */
    private int getIntFrom( int beginIndex,
                            int endIndex,
                            String fieldName,
                            String timeDate,
                            String sourceRecord )
        throws BasicException
    {
        String myPortion = timeDate.substring( beginIndex, endIndex );
        int value = 0;
        try
        {
            value = Integer.parseInt( myPortion, 10 );
        }
        catch ( NumberFormatException e )
        {
            String message = fieldName +
                             " value of \"" +
                             myPortion +
                             "\" is not a valid integer";
            reportFormatError( message, timeDate, sourceRecord );
        }
        return value;
    }

    /**
     * Report an error in the format of the magnet data.
     *
     * @param message The message detailing the error.
     *
     * @param field The field in which the error was detected.
     *  If null, the field description is omitted.
     *
     * @param sourceRecord The source record from the source ASCII magnet data.
     *
     * @exception AscServiceAbortException Always throws the exception to
     *  indicate that the service should be aborted.
     */
    private void reportFormatError( String message,
                                    String field,
                                    String sourceRecord )
        throws BasicException
    {
        StringBuffer buffer = new StringBuffer( IRI_BUFFER_SIZE );
        String lineSeparator = System.getProperty( "line.separator", "\n" );
        buffer.append( "Error: " );
        buffer.append( message );
        buffer.append( lineSeparator );
        if ( field != null )
        {
            buffer.append( "  Detected in field: " );
            buffer.append( field );
            buffer.append( lineSeparator );
        }
        buffer.append( "  Occurred in line: " );
        buffer.append( sourceRecord );
        throw new BasicException(buffer.toString());
    }

    /**
     * Verify that the indices for each of the channels are in range.
     *
     * @exception AscServiceAbortException If a channel index is out of range.
     */
    private void verifyChannelIndices() throws BasicException
    {
        for ( int idx = 0; idx < theChannelDefList.length; idx += 1 )
        {
            MagnetDataChannelDef chan = theChannelDefList[idx];
            final int chanIdx = chan.getChannelIndex();
            if ( chanIdx >= channelValues.length )
            {
                String message = "Channel index, " +
                                 chanIdx +
                                 ", for " +
                                 chan.getChannelName() +
                                 " channel is out of range.  Only " +
                                 channelValues.length +
                                 " channel values are defined.";
                throw new BasicException(message);
            }
        }
    }

    /**
     * Generate a representation of the IRI data as name/value pairs.
     *
     * @return The IRI data list, as name/value pairs.
     *
     * @exception AscServiceAbortException If a channel index is out of range.
     */
    public String getNameValuePairs() throws BasicException
    {
        StringBuffer buffer = new StringBuffer( IRI_BUFFER_SIZE );
        buffer.append( dateAsNameValuePairs );
        for ( int idx = 0; idx < theChannelDefList.length; idx += 1 )
        {
            MagnetDataChannelDef chan = theChannelDefList[idx];
            buffer.append( '&' );
            buffer.append( chan.getChannelName() );
            buffer.append( '=' );
            buffer.append( formatChannelValue( chan ) );
        }
        return buffer.toString();
    }

    /**
     * Format the specified channel value to the appropriate IRI format.
     *  If the value is out of range, it is silently trimmed to its
     *   appropriate limit value.  The lower limit is assumed to be zero.
     *
     * @param channelDef Channel information for the specified channel.
     *
     * @return The formatted channel value.
     *
     * @exception AscServiceAbortException If unable to format the value.
     */
    private String formatChannelValue( MagnetDataChannelDef channelDef )
        throws BasicException
    {
        final float rawValue = channelValues[ channelDef.getChannelIndex() ];
        float value = Math.max( 0.0f, rawValue );
        value = Math.min( value, channelDef.getChannelMaxValue() );
        String returnString = null;
        try
        {
            theFormatter.applyPattern( channelDef.getFormatString() );
            returnString = theFormatter.format( value );
        }
        catch ( IllegalArgumentException e )
        {
            String message = "Unable to format " +
                             channelDef.getChannelName() +
                             " channel.  Magnet data value: " +
                             rawValue +
                             "  Trimmed value: " +
                             value +
                             "  Format: " +
                             channelDef.getFormatString() +
                             "  Error: " +
                             e.getMessage();
            throw new BasicException(message);
        }
        return returnString;
    }

    /**
     * Determine whether the specified channel is enabled.  A channel whose
     *  status was not in the magnet data is assumed to be enabled.
     *
     * @param chan The channel being queried.
     *
     * @return True if the channel is enabled, false if disabled.
     */
    private boolean isEnabled( MagnetDataChannelDef chan )
    {
        final int chanIdx = chan.getChannelIndex();
        int status = 0;
        if ( chanIdx < channelStatus.length )
        {
            status = channelStatus[chanIdx];
        }
        if ( (status & 0x01) == 0 )
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}
